﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace MetalSoda
{
    /// <summary>
    /// Provides database properties
    /// </summary>
    [DebuggerDisplay("{Display}")]
    [Serializable]
    public sealed class DatabaseInfo
    {
        #region Private Fields
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        readonly OneToOneAssociation<DatabaseInfo, ServerInfo> m_server;
        readonly OneToManyAssociation<DatabaseInfo, ServiceHost> m_hosts;

        readonly LazyObject<string> ConnectionStringLazy;
        readonly LazyObject<string> DisplayStringLazy;        

        #endregion

        public void AddServiceHost(ServiceHost serviceHost)
        {
            m_hosts.Add(serviceHost);
        }

        public void RemoveServiceHost(ServiceHost serviceHost)
        {
            m_hosts.Remove(serviceHost);
        }


        #region Properties and Read-only Fields

        /// <summary>
        /// Display string (read-only)
        /// </summary>        
        public string Display { [DebuggerStepThrough] get { return DisplayStringLazy.Value; } }

        /// <summary>
        /// Database connection string (read-only)
        /// </summary>
        public string ConnectionString { [DebuggerStepThrough] get { return ConnectionStringLazy.Value; } }

        /// <summary>
        /// Database name (read-only)
        /// </summary>
        public readonly string DatabaseName;

        /// <summary>
        /// Gets the database server
        /// </summary>
        public ServerInfo Server
        {
            [DebuggerStepThrough]
            get { return m_server.Ref; }
            internal set { m_server.Ref = value; }
        }

        public Guid? SSBIdentifier { get; internal set; }

        #endregion

        #region Constructors


        /// <summary>
        /// Initializes a new instance of the <see cref="DatabaseInfo"/> class.
        /// </summary>
        /// <param name="database">Database name</param>
        /// <param name="instance">Server name</param>
        /// <param name="autoCreate">Informs this constructor on whether the database must be created if does not exist yet.</param>
        internal DatabaseInfo(string database, ServerInfo instance, bool autoCreate = false)
        {
            m_server = new OneToOneAssociation<DatabaseInfo, ServerInfo>(this,
                assigned: args => args.Ref.IncludeDatabase(this),
                unassigned: delegate { });
            m_hosts = new OneToManyAssociation<DatabaseInfo, ServiceHost>(this,
                added: args => args.Ref.AddDatabase(this),
                removed: args => args.Ref.RemoveDatabase(this));

            if (database == null) { throw new ArgumentNullException("database"); }
            if (instance == null) { throw new ArgumentNullException("instance"); }

            this.Server = instance;

            DatabaseName = database = DatabaseInfo.NormalizeDatabaseName(database);

            ConnectionStringLazy = new LazyObject<string>(() => CreateConnectionString(autoCreate));
            DisplayStringLazy = new LazyObject<string>(() => string.Format("[{0}].[{1}]", Server.ServerName, DatabaseName));
        }




        private string CreateConnectionString(bool autoCreate)
        {
            var connectionString = new SqlConnectionStringBuilder(Server.MasterDBConnectionString) { InitialCatalog = DatabaseName }.ConnectionString;
            if (!autoCreate) { return connectionString; }
            using (var connection = new SqlConnection(Server.MasterDBConnectionString))
            {
                try { connection.Open(); }
                catch (InvalidOperationException ex)
                {
                    var message = new StringBuilder();
                    throw new MetalSodaException(message.ToString(), ex);
                }
                catch (SqlException ex)
                {
                    var message = new StringBuilder()
                    .AppendFormat("Failed on probing for the [{0}].[{1}] database existance. Ensure that the [{0}] SQL Server does exist and is accessible.", Server.ServerName, this.DatabaseName);
                    throw new MetalSodaException(message.ToString(), ex);
                }

                var command = new SqlCommand(new SQLScript.CreateDatabaseRuntimeTextTemplate(this.DatabaseName).TransformText(), connection);
                command.ExecuteNonQuery();
            }
            return connectionString;
        }

        #endregion
        

        /// <summary>
        /// Determines whether this instance and a specified <see cref="DatabaseInfo"/> object refer to the same database.
        /// </summary>
        /// <param name="other">The <see cref="DatabaseInfo"/> object to compare to this instance.</param>
        /// <returns>True if this instance and the specified <see cref="DatabaseInfo"/> object refer to the same database.</returns>
        public bool Match(DatabaseInfo other)
        {
            return Object.ReferenceEquals(this, other) || this.Display.Equals(other.Display, StringComparison.OrdinalIgnoreCase);
        }

        #region Private Methods
        /// <summary>
        /// Tests whether the specified database exists on the server and if not, create it.
        /// </summary>
        /// <param name="builder">SQL connection builder identifying the database.</param>
        private static void CreateDatabaseIfNotFound(SqlConnectionStringBuilder builder)
        {
            var masterDbConnectionString = new SqlConnectionStringBuilder(builder.ConnectionString) { InitialCatalog = "master" }.ConnectionString;
            using (var connection = new SqlConnection(masterDbConnectionString))
            {
                try { connection.Open(); }
                catch (InvalidOperationException ex)
                {
                    var message = new StringBuilder();
                    throw new MetalSodaException(message.ToString(), ex);
                }
                catch (SqlException ex)
                {
                    var message = new StringBuilder()
                    .AppendFormat("Failed on probing for the [{0}].[{1}] database existance. Ensure that the [{0}] SQL Server does exist and is accessible.", builder.DataSource, builder.InitialCatalog);
                    throw new MetalSodaException(message.ToString(), ex);
                }

                var command = new SqlCommand(new SQLScript.CreateDatabaseRuntimeTextTemplate(builder.InitialCatalog).TransformText(), connection);
                command.ExecuteNonQuery();
            }
        }

        /// <summary>
        /// Retrieves the server name
        /// </summary>
        /// <param name="connectionString">Database connection string.</param>
        /// <returns>Server name</returns>
        private static string GetServerName(string connectionString)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                try { connection.Open(); }
                catch (InvalidOperationException ex)
                {
                    var message = new StringBuilder();
                    throw new MetalSodaException(message.ToString(), ex);
                }
                catch (SqlException ex)
                {
                    var message = new StringBuilder();
                    throw new MetalSodaException(message.ToString(), ex);
                }
                return (string)new SqlCommand("SELECT @@SERVERNAME", connection).ExecuteScalar();
            }
        }

        /// <summary>
        /// Creates the formatted display string
        /// </summary>
        /// <param name="server">Server name</param>
        /// <param name="database">Database name</param>
        /// <returns>Display string</returns>
        private static string CreateDisplayString(string server, string database) { return string.Format("[{0}].[{1}]", server, database); }

        /// <summary>
        /// Creates the database name normalized string
        /// </summary>
        /// <param name="database">Database  name</param>
        /// <returns>Normalized database name</returns>
        internal static string NormalizeDatabaseName(string database)
        {
            var regex = new Regex(@"(?:^@exp$)|(?:^\s*\[@exp\]\s*$)".Replace("@exp", @"(?<name>\w(?:[ \t.]*\w+)*)"));
            var match = regex.Match(database);
            if (match.Success) { return match.Groups["name"].Value; }
            var message = new StringBuilder();
            message.AppendFormat(@"""{0}"" is not a valid database name", database);
            throw new MetalSodaException(message.ToString());
        }

        /// <summary>
        /// Creates the server name normalized string
        /// </summary>
        /// <param name="server">Server name</param>
        /// <returns>Normalized server name</returns>
        private static string NormalizeServerName(string server)
        {
            var regex = new Regex(@"(?:^@exp$)|(?:^\s*\[@exp\]\s*$)".Replace("@exp", @"(?<name>\w(?:[ \t.-]*\w+)*)"));
            var match = regex.Match(server);
            if (match.Success) { return match.Groups["name"].Value; }
            var message = new StringBuilder();
            message.AppendFormat(@"""{0}"" is not a valid server name", server);
            throw new MetalSodaException(message.ToString());
        } 
        #endregion

        internal void DeployDatabaseConfigurations()
        {            
            using (var connection = new SqlConnection(ConnectionString))
            {
                connection.Open();

                var command = new SqlCommand(
                    new SQLScript.EnableBrokerRuntimeTextTemplate(this.DatabaseName).TransformText(),
                    connection);
                
                object obj = command.ExecuteScalar();
                this.SSBIdentifier = (Guid)obj;
            }
        }
    } 
}
